local open = io.open
local assert = assert
local type = type
local ipairs = ipairs
local pairs = pairs
local tostring = tostring
local tonumber = tonumber
local select = select
local error = error
local pcall = pcall
local setmetatable = setmetatable
local log = log

module("db")

local function tovalue(name, value)
	-- print("tovalue", name, value)
	value = value and tostring(value)
	if name == "string" then
		if value and value:sub(1, 1) == "\"" then
			value = loadstring("return "..value)()
		end
		return value
	elseif name == "number" then
		return tonumber(value)
	elseif name == "boolean" then
		if value then
			if value == "false" or value == "0" or value == "no" then
				value = nil
			end
			return value and true or false
		end
	else
		error(("invalid type name %q"):format(name))
	end
end

local function tolist(base, ...)
	local r = {}
	for i = 1, select("#", ...) do
		r[i] = tovalue(base, select(i, ...):trim())
	end
	return r
end

local function tokeylist(base, ...)
	local r = {}
	for i = 1, select("#", ...) do
		r[tovalue(base, select(i, ...):trim())] = true
	end
	return r
end

local function validate_value(typeinfo, s)
	-- print("validate_value", typeinfo.type, s)
	if not s then return end
	local typename = typeinfo.type
	if typename == "list" then
		return tolist(typeinfo.base, (","):split(s))
	elseif typename == "keylist" then
		return tokeylist(typeinfo.base, (","):split(s))
	else
		return tovalue(typename, s)
	end
end

local function validate_type(typename, param)
	-- print("validate_type", typename, param)
	local info = {}
	if typename == "string" or typename == "number" or typename == "boolean" then
		info.type = typename
	elseif typename == "list" or typename == "keylist" then
		local base, default = (","):split(param, 2)

		info.type = typename
		info.base = base:trim()
		param = default
	else
		error(("invalid type name %q"):format(typename))
	end
	local success, value = pcall(validate_value, info, param)
	if success then
		-- print("set_default", typename, value)
		info.default = value
	end
	return info
end

local function validate_proto(prototype)
	-- print("validate_proto")
	for name, typestring in pairs(prototype) do
		prototype[name] = validate_type((","):split(typestring, 2))
	end
end

local function validate_entry(entry, prototype)
	-- print("validate_entry")
	local clean = {}
	for name in pairs(entry) do
		assert(prototype[name])
	end
	for name, info in pairs(prototype) do
		local value = validate_value(info, entry[name])
		entry[name] = value or info.default
	end
end

local function read_database(filename)
	-- print("read_database", filename)
	local f = assert(open(filename, "r"))
	local prototype, prototype_valid
	local database = {}
	local entry, entryname
	while true do
		local line = f:read()
		if not line then break end
		local p = line:find(";", 1, 1)
		if p then
			line = line:sub(1, p - 1)
		end
		line = line:trim()
		if line:len() > 0 then
			local name, key, value
			name = line:match("^%[(.*)%]$")
			if name then
				-- print("get", "name", name)
				if name == "prototype" then
					assert(not prototype)
					prototype = {}
					entry = prototype
				else
					assert(prototype)
					if not prototype_valid then
						validate_proto(prototype)
						prototype_valid = true
					else
						validate_entry(entry, prototype)
					end
					entry = {}
					local index = tonumber(name)
					if index and tostring(index) == name then
						name = index
					end
					database[name] = entry
				end
			else
				key, value = line:match("^([^=]+)=(.*)$")
				assert(key and value and entry)
				-- print("get", "keyvalue", key, value)
				entry[key:trim()] = value:trim()
			end
		end
	end
	f:close()
	if entry and entry ~= prototype then
		validate_entry(entry, prototype)
	end
	return database
end

local function fill_table_entries(prototype, ...)
	if prototype == false and select("#", ...) == 1 then
		return ...
	end
	local table = {}
	for i = 1, select("#", ...) do
		local index = prototype and prototype[i] or i
		if index == "-" then index = i end
		table[index] = select(i, ...):trim()
	end
	return table
end

local function read_table(filename)
	local f = assert(open(filename, "r"))
	local db = {}
	local prototype
	while true do
		local line = f:read()
		if not line then break end
		local p = line:find(";", 1, 1)
		if p then
			line = line:sub(1, p - 1)
		end
		line = line:trim()
		if line:len() > 0 then
			if prototype == nil then
				if line == "-" then
					prototype = false
				else
					prototype = fill_table_entries(nil, (","):split(line))
				end
			else
				db[#db + 1] = fill_table_entries(prototype, (","):split(line))
			end
		end
	end
	f:close()
	return db
end

local cache = setmetatable({}, {
	__index = function (table, name)
		local status, db = pcall(read_database, "db/"..name..".db")
		if not status then
			status, db = pcall(read_table, "db/"..name..".table")
		end
		if status then
			table[name] = db
			return db
		else
			log("EE db.cache error", name, "not found")
		end
	end,
})

function get(name) return cache[name] end

local f = function (_, key) return get(key) end
setmetatable(_M, {
	__index = f,
	__call = f,
})
